메시지 전달
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;