본문으로 건너뛰기

메시지 전달

ControlBee의 액터는 비동기 메시지를 통해 통신합니다. 이는 액터를 분리하고 유연하고 추적 가능한 상호작용을 가능하게 합니다.

메시지 구조

메시지는 발신자 참조, 메시지 이름 및 선택적 데이터 페이로드를 포함합니다:

using Dict = System.Collections.Generic.Dictionary<string, object?>;

public class Message
{
public IActor Sender { get; set; }
public string Name { get; set; }
public object? Data { get; set; }
public Dict? DictPayload { get; set; }
}

메시지 전송

메시지 게시

액터는 자신이나 피어에게 메시지를 게시합니다:

// 자신에게 전송
Publish(new Message(this, "Start"));

// 피어에게 전송
NextStage.Publish(new Message(this, "Transfer"));

// 간단한 데이터와 함께 전송
Picker.Publish(new Message(this, "Pick", coordinates));

// Dict를 사용한 복잡한 데이터 전송
Picker.Publish(new Message(this, "Pick", new Dict
{
["X"] = 100.5,
["Y"] = 200.3,
["Z"] = 50.0
}));

브로드캐스트

여러 액터에게 메시지를 전송합니다:

foreach (var peer in Peers)
{
peer.Publish(new Message(this, "Stop"));
}

메시지 처리

메시지는 액터의 현재 상태에 의해 처리됩니다:

public class IdleState(ConveyorActor actor) : State<ConveyorActor>(actor)
{
public override bool ProcessMessage(Message message)
{
switch (message.Name)
{
case "Load":
Actor.State = new LoadingState(Actor);
return true; // Message handled

case "GetStatus":
return false; // Not handled, try base class

default:
return false; // Unknown message
}
}
}

반환값:

  • true - 메시지가 처리되었습니다
  • false - 메시지가 처리되지 않았으며, 부모 또는 기본 핸들러를 시도합니다

DictPayload 처리

메시지에 Dict 페이로드가 포함된 경우 DictPayload를 통해 액세스합니다:

public override bool ProcessMessage(Message message)
{
switch (message.Name)
{
case "LoadProject":
if (message.DictPayload != null)
{
var index = message.DictPayload.GetValueOrDefault("Index") as int?;
var filePath = message.DictPayload.GetValueOrDefault("ProjectFile") as string;

if (filePath != null)
{
LoadProject(index ?? 0, filePath);
}
}
return true;

case "UpdatePosition":
if (message.DictPayload != null)
{
var x = message.DictPayload.GetValueOrDefault("X") as double?;
var y = message.DictPayload.GetValueOrDefault("Y") as double?;
var z = message.DictPayload.GetValueOrDefault("Z") as double?;

if (x.HasValue && y.HasValue && z.HasValue)
{
UpdatePosition(x.Value, y.Value, z.Value);
}
}
return true;

default:
return false;
}
}

내장 메시지

ControlBee는 여러 시스템 메시지를 제공합니다:

StateEntryMessage

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

case StateEntryMessage.MessageName:
// Initialize state
Actor.SetStatus("Ready", true);
StartTimer(100); // Periodic scan
return true;

StateExitMessage

상태를 떠날 때 자동으로 전송됩니다:

case StateExitMessage.MessageName:
// Clean up resources
StopTimer();
Actor.SetStatus("Ready", false);
return true;

TimerMessage

액터 생성자에서 구성되었을 때 스캔 작업을 위해 주기적으로 전송됩니다.

구성:

public FlipperActor(ActorConfig config) : base(config)
{
TimerMilliseconds = 200; // 200ms 간격으로 타이머 활성화
// ... 나머지 초기화
}

처리:

case TimerMessage.MessageName:
return Scan(); // 센서 확인, 상태 업데이트

private bool Scan()
{
if (SensorChanged())
{
UpdateStatus();
return true;
}
return false;
}

사용 사례:

  • 센서 폴링 및 상태 모니터링
  • 다른 액터에 대한 주기적인 상태 업데이트
  • 타임아웃 감지
  • 백그라운드 상태 확인

상태 통신

메시지 외에도 액터는 상태 속성을 통해 통신합니다:

상태 설정

// Set local status
SetStatus("Busy", true);
SetStatus("Position", currentPos);

// Set status on another actor (via Syncer)
SetStatusByActor(Syncer, "ReadyToLoad", requestId);

상태 읽기

// Get peer status
bool ready = GetPeerStatus(NextStage, "Ready") is true;
int position = (int)(GetPeerStatus(Head, "Position") ?? 0);

// Check system mode
if (GetPeerStatus(Syncer, "RunMode") is RunModeEnum.Auto)
{
// Auto mode behavior
}

상태 vs. 메시지

상태를 사용하는 경우:

  • 상태가 연속적입니다 (busy/idle, position, temperature)
  • 여러 액터가 동일한 정보를 조회해야 합니다
  • 최신 값이 중요합니다 (이력이 아님)

메시지를 사용하는 경우:

  • 이벤트가 이산적입니다 (start, stop, error occurred)
  • 조치가 필요합니다 (load, unload, initialize)
  • 순서가 중요합니다 (순차적 작업)

메시지 패턴

요청-응답 패턴

한 액터가 요청하고 다른 액터가 응답합니다:

// Requestor
public void RequestPermission()
{
SetStatusByActor(Syncer, "TransferRequest", Guid.NewGuid());
}

// Responder (Syncer)
case TimerMessage.MessageName:
if (HasPendingRequest())
{
SetStatus("Grant", requestorName);
}
return false;

// Requestor checks response
if (GetPeerStatus(Syncer, "Grant") == Name)
{
// Permission granted
}

명령 패턴

작업을 수행하기 위한 직접 명령입니다:

// Commander
if (allReady)
{
Head.Publish(new Message(this, "Pick", new Dict
{
["Position"] = position,
["Speed"] = 100.0
}));
}

// Receiver
case "Pick":
if (message.DictPayload != null)
{
var pos = message.DictPayload.GetValueOrDefault("Position") as Position3D;
var speed = message.DictPayload.GetValueOrDefault("Speed") as double?;

if (pos != null)
{
State = new PickingState(Actor, pos, speed ?? 50.0);
}
}
return true;

옵저버 패턴

액터가 피어 상태 변경을 관찰합니다:

// Observer
case TimerMessage.MessageName:
bool newAutoMode = GetPeerStatus(Syncer, "_auto") is true;

if (newAutoMode != wasAutoMode)
{
wasAutoMode = newAutoMode;

if (newAutoMode)
State = new AutoState(Actor);
else
State = new IdleState(Actor);
}
return false;

상태 동기화

액터가 주기적으로 상태를 동기화합니다:

private bool Scan()
{
// Check if peer has failed
if (Actor.HasPeerFailed(Actor.Syncer))
{
Actor.State = new ErrorState(Actor);
return true;
}

// Check for new work
bool hasWork = Actor.GetPeerStatus(Actor.Syncer, "WorkAvailable") is true;
if (hasWork && !Actor.Busy.Value)
{
Actor.State = new WorkingState(Actor);
return true;
}

return false;
}

메시지 순서

메시지는 각 액터에 의해 순차적으로 처리됩니다:

Actor Queue
┌─────────┐
│ Start │ ← Processed first
├─────────┤
│ Load │
├─────────┤
│ Unload │ ← Processed last
└─────────┘

이는 다음을 보장합니다:

  • 일관된 상태 - 경합 조건 없음
  • 예측 가능한 동작 - 결정론적 순서
  • 스레드 안전성 - 잠금이 필요하지 않음

메시지 흐름 예제

센서 감지에서 작업까지의 완전한 흐름입니다:

// 1. Sensor detects material (Conveyor)
case TimerMessage.MessageName:
if (TrayDet.IsOnOrTrue() && !Exists.Value)
{
Exists.Value = true;
SetStatusByActor(Syncer, "MaterialArrived", Guid.NewGuid());
}
return false;

// 2. Syncer coordinates (Syncer)
case TimerMessage.MessageName:
if (ConveyorHasMaterial() && StageIsReady())
{
SetStatus("Grant", "Conveyor");
}
return false;

// 3. Conveyor gets permission (Conveyor)
case TimerMessage.MessageName:
if (GetPeerStatus(Syncer, "Grant") == Name)
{
Stage.Publish(new Message(this, "PrepareLoad"));
State = new TransferringState(Actor);
}
return false;

// 4. Stage prepares (Stage)
case "PrepareLoad":
Actor.State = new PreparingState(Actor);
return true;

// 5. Stage loads (Stage)
public class PreparingState : State<StageActor>
{
public override bool ProcessMessage(Message message)
{
case StateEntryMessage.MessageName:
Actor.MoveToLoadPosition();
Actor.SetStatus("ReadyForLoad", true);
return true;
}
}

모범 사례

1. 설명적인 메시지 이름 사용

Publish(new Message(this, "StartTransfer"));  // ✅ Clear intent
Publish(new Message(this, "Msg1")); // ❌ Unclear

2. 모든 메시지 처리

처리되지 않은 메시지에 대해 false를 반환합니다:

public override bool ProcessMessage(Message message)
{
switch (message.Name)
{
case "KnownMessage":
// Handle
return true;
default:
return false; // Let parent/default handler try
}
}

3. 메시지 핸들러에서 차단 방지

case "Move":
// ❌ Bad - blocks message processing
X.MoveAndWait(target);
return true;

case "Move":
// ✅ Good - transitions to state that handles motion
Actor.State = new MovingState(Actor, target);
return true;

4. 쿼리에 상태 사용

// ❌ 피하기 - 메시지 왕복이 필요합니다
Peer.Publish(new Message(this, "GetPosition"));
// ... 응답 메시지 대기 ...

// ✅ 더 나음 - 직접 상태 읽기
int pos = (int)(GetPeerStatus(Peer, "Position") ?? 0);

5. StateExitMessage에서 정리

case StateExitMessage.MessageName:
StopTimer();
CancelPendingOperations();
SetStatus("Busy", false);
return true;

참고