액터 시스템
ControlBee는 액터 기반 아키텍처를 중심으로 구축되어 있으며, 자동화 시스템의 각 구성 요소는 메시지를 통해 다른 액터와 통신하는 독립적인 액터로 모델링됩니다.
액터란 무엇인가?
액터는 다음과 같은 특징을 가진 독립적인 계산 단위입니다:
- 상태 캡슐화 - 자체 내부 데이터를 관리합니다
- 메시지 처리 - 들어오는 메시지에 비동기적으로 응답합니다
- 메시징을 통한 통신 - 다른 액터에게 메시지를 전송합니다
- 격리 유지 - 다른 액터의 상태에 직접 접근할 수 없습니다
왜 액터를 사용하나요?
액터 모델은 자동화 시스템에 여러 이점을 제공합니다:
1. 자연스러운 분해
물리적 기계는 자연스럽게 액터로 분해됩니다:
- 컨베이어 - 재료 운송
- 스테이지 - 작업물 고정
- 헤드 - 도구 위치 지정
- 피커 - 부품 조작
각 액터는 명확한 책임을 가진 실제 구성 요소를 나타냅니다.
2. 동시 실행
액터는 독립적으로 실행되며 병렬로 작동할 수 있습니다:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Stage │ │ Head │ │ Picker │
│ Actor │◄────►│ Actor │◄────►│ Actor │
└─────────┘ └─────────┘ └─────────┘
│ │ │
Loading Moving Gripping
(parallel) (parallel) (parallel)
3. 장애 격리
액터가 실패해도 전체 시스템이 중단되지 않습니다. 다른 액터는 계속 작동하거나 장애를 우아하게 처리할 수 있습니다.
4. 메시지 기반 조정
액터는 잘 정의된 메시지를 통해 조정되므로 상호작용이 명시적이고 추적 가능합니다.
액터 생성
액터는 Actor 기본 클래스를 상속합니다:
public class ConveyorActor : Actor
{
// Variables - Actor's state
public Variable<bool> Exists = new(VariableScope.Temporary);
public Variable<int> ConveyorSpeed = new(VariableScope.Local, 100);
// IO - Hardware interfaces
public IDigitalInput TrayDet = new DigitalInputPlaceholder();
public IDigitalOutput ConveyorRun = new DigitalOutputPlaceholder();
// Peers - References to other actors
public IActor Syncer = null!;
public IActor NextStage = null!;
public ConveyorActor(ActorConfig config) : base(config)
{
State = new IdleState(this);
}
public void SetPeers(IActor syncer, IActor nextStage)
{
Syncer = syncer;
NextStage = nextStage;
InitPeers([Syncer, NextStage]);
}
}
액터 수명 주기
1. 생성
액터는 ActorFactory를 통해 생성됩니다:
var actorFactory = serviceProvider.GetRequiredService<ActorFactory>();
var conveyor = actorFactory.Create<ConveyorActor>("InConveyor");
var stage = actorFactory.Create<StageActor>("Stage0", StageType.Tray);
2. 피어 설정
액터는 피어와의 관계를 설정합니다:
conveyor.SetPeers(syncer, stage);
stage.SetPeers(syncer, head, camera);
이는 액터 네트워크를 생성합니다:
┌─────────┐
│ Syncer │ (Orchestrator)
└────┬────┘
│
┌───────┼───────┐
│ │ │
┌───▼───┐ ┌─▼─────┐ ┌──▼────┐
│Conveyor│ │ Stage │ │ Head │
└────────┘ └───────┘ └───────┘
3. 변수 로딩
변수는 저장된 레시피에서 로드됩니다:
variableManager.Load(recipeName);
이는 VariableScope.Local 변수를 레시피별 값으로, VariableScope.Global 변수를 공유 값으로 복원합니다.
4. 시작
액터가 메시지 처리를 시작합니다:
syncer.Start();
conveyor.Start();
stage.Start();
각 액터는 초기 상태로 진입하고 메시지 처리를 시작합니다.
액터 구성 요소
변수
액터는 타입 변수를 사용하여 상태를 관리합니다:
// Global - 모든 레시피에서 공유, 자동 저장
public Variable<double> HomeSpeed = new(VariableScope.Global, 10.0);
public Variable<double> SafetyMargin = new(VariableScope.Global, 5.0);
// Local - 레시피별로 다른 값, 자동 저장
public Variable<double> Speed = new(VariableScope.Local, 100.0);
public Variable<Position1D> LoadPos = new(VariableScope.Local);
// Temporary - 런타임 상태, 자동 저장 안 됨
public Variable<bool> Busy = new(VariableScope.Temporary);
public Variable<int> CurrentIndex = new(VariableScope.Temporary);
자세한 내용은 변수 시스템을 참조하세요.
장치
액터는 장치 인터페이스를 통해 하드웨어와 인터페이스합니다:
// Digital IO
public IDigitalInput SensorIn = new DigitalInputPlaceholder();
public IDigitalOutput ValveOut = new DigitalOutputPlaceholder();
// Motion control
public IAxis X = config.AxisFactory.Create();
// Vision
public IVision Camera = new VisionPlaceholder();
자세한 내용은 장치 통합을 참조하세요.
상태 머신
액터는 상태 머신을 사용하여 동작을 관리합니다:
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;
case "Unload":
Actor.State = new UnloadingState(Actor);
return true;
}
return false;
}
}
자세한 내용은 상태 관리를 참조하세요.
피어 통신
액터는 상태와 메시지를 통해 통신합니다:
// Read peer status
bool ready = GetPeerStatus(NextStage, "Ready") is true;
// Set status for peers to read
SetStatus("Loaded", true);
// Send message to peer
NextStage.Publish(new Message("Transfer"));
자세한 내용은 메시지 전달을 참조하세요.
일반적인 액터 패턴
오케스트레이터 패턴
Syncer 액터가 여러 액터를 조정합니다:
public class SyncerActor : Actor
{
public IActor Head, Stage, Conveyor;
// Grant permission to actors
public void GrantTransfer(IActor requestor)
{
SetStatus("Grant", requestor.Name);
}
}
// Actors check for permission
if (GetPeerStatus(Syncer, "Grant") == Name)
{
// I have permission to act
}
생산자-소비자 패턴
액터가 재료 흐름을 조정합니다:
// Producer (Conveyor)
if (Exists.Value && NextStageReady())
{
SetStatusByActor(Syncer, "ReadyToUnload", trackingId);
}
// Consumer (Stage)
if (!HasMaterial && PrevStageReady())
{
SetStatusByActor(Syncer, "ReadyToLoad", trackingId);
}
// Orchestrator matches producer with consumer
if (HasReadyProducer() && HasReadyConsumer())
{
GrantTransfer(producer, consumer);
}
안전 검사 패턴
액터는 작업 전에 전제 조건을 확인합니다:
public void EnsureReady()
{
if (GetPeerStatus(Syncer, "_auto") is not true)
throw new SequenceError("System not in auto mode");
if (HasPeerFailed(Syncer))
throw new SequenceError("Peer actor has failed");
if (!SensorIn.IsOnOrTrue())
{
ErrorDialog.Show();
throw new SequenceError("Safety sensor not detected");
}
}
모범 사례
1. 단일 책임
각 액터는 명확하고 집중된 목적을 가져야 합니다:
- ✅ ConveyorActor - 재료 운송만 처리합니다
- ❌ MachineActor - 너무 광범위하며 모든 것을 수행합니다
2. 명시적 피어 관계
피어 액터를 명시적으로 정의합니다:
public IActor Syncer = null!; // Required orchestrator
public IActor? NextStage; // Optional downstream actor
3. 방어적 상태 검사
작업 전에 항상 상태를 확인합니다:
public void Load()
{
if (Exists.Value)
throw new SequenceError("Already have material");
// ... proceed with load
}
4. 의미 있는 상태 키
설명적인 상태 키를 사용합니다:
SetStatus("ReadyToTransfer", true); // ✅ Clear intent
SetStatus("Flag1", true); // ❌ Unclear meaning
5. 우아한 초기화
초기화 실패를 처리합니다:
public override void Init(ActorConfig config)
{
base.Init(config);
X.SetInitializeAction(() =>
{
try
{
InitializeAxis(X);
}
catch (Exception ex)
{
InitError.Show($"Failed to initialize X axis: {ex.Message}");
throw;
}
});
}