Actor System
ControlBee is built around an actor-based architecture where each component of your automation system is modeled as an independent actor that communicates with other actors through messages.
What is an Actor?
An Actor is a self-contained computational unit that:
- Encapsulates state - Manages its own internal data
- Processes messages - Responds to incoming messages asynchronously
- Communicates through messaging - Sends messages to other actors
- Maintains isolation - Cannot directly access other actors' state
Why Actors?
The actor model provides several benefits for automation systems:
1. Natural Decomposition
Physical machines naturally decompose into actors:
- Conveyor - Material transport
- Stage - Workpiece holding
- Head - Tool positioning
- Picker - Part manipulation
Each actor represents a real-world component with clear responsibilities.
2. Concurrent Execution
Actors run independently and can operate in parallel:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Stage │ │ Head │ │ Picker │
│ Actor │◄────►│ Actor │◄────►│ Actor │
└─────────┘ └─────────┘ └─────────┘
│ │ │
Loading Moving Gripping
(parallel) (parallel) (parallel)
3. Fault Isolation
When an actor fails, it doesn't crash the entire system. Other actors can continue or handle the failure gracefully.
4. Message-Driven Coordination
Actors coordinate through well-defined messages, making interactions explicit and traceable.
Creating an Actor
Actors inherit from the Actor base class:
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]);
}
}
Actor Lifecycle
1. Creation
Actors are created through the ActorFactory:
var actorFactory = serviceProvider.GetRequiredService<ActorFactory>();
var conveyor = actorFactory.Create<ConveyorActor>("InConveyor");
var stage = actorFactory.Create<StageActor>("Stage0", StageType.Tray);
2. Peer Setup
Actors establish relationships with their peers:
conveyor.SetPeers(syncer, stage);
stage.SetPeers(syncer, head, camera);
This creates a network of actors:
┌─────────┐
│ Syncer │ (Orchestrator)
└────┬────┘
│
┌───────┼───────┐
│ │ │
┌───▼───┐ ┌─▼─────┐ ┌──▼────┐
│Conveyor│ │ Stage │ │ Head │
└────────┘ └───────┘ └───────┘
3. Variable Loading
Variables are loaded from saved recipes:
variableManager.Load(recipeName);
This restores VariableScope.Local variables to their recipe-specific values and VariableScope.Global variables to their shared values.
4. Start
Actors begin processing messages:
syncer.Start();
conveyor.Start();
stage.Start();
Each actor enters its initial state and begins handling messages.
Actor Components
Variables
Actors use typed variables to manage state:
// Global - Shared across all recipes, auto-saved
public Variable<double> HomeSpeed = new(VariableScope.Global, 10.0);
public Variable<double> SafetyMargin = new(VariableScope.Global, 5.0);
// Local - Different value per recipe, auto-saved
public Variable<double> Speed = new(VariableScope.Local, 100.0);
public Variable<Position1D> LoadPos = new(VariableScope.Local);
// Temporary - Runtime state, not auto-saved
public Variable<bool> Busy = new(VariableScope.Temporary);
public Variable<int> CurrentIndex = new(VariableScope.Temporary);
See Variable System for details.
Devices
Actors interface with hardware through device interfaces:
// 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();
See Device Integration for details.
State Machine
Actors use state machines to manage behavior:
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;
}
}
See State Management for details.
Peer Communication
Actors communicate through status and messages:
// 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"));
See Message Passing for details.
Common Actor Patterns
Orchestrator Pattern
A Syncer actor coordinates multiple actors:
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-Consumer Pattern
Actors coordinate material flow:
// 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);
}
Safety Checking Pattern
Actors verify preconditions before operations:
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");
}
}
Best Practices
1. Single Responsibility
Each actor should have a clear, focused purpose:
- ✅ ConveyorActor - Handles material transport only
- ❌ MachineActor - Too broad, does everything
2. Explicit Peer Relationships
Define peer actors explicitly:
public IActor Syncer = null!; // Required orchestrator
public IActor? NextStage; // Optional downstream actor
3. Defensive State Checks
Always verify state before operations:
public void Load()
{
if (Exists.Value)
throw new SequenceError("Already have material");
// ... proceed with load
}
4. Meaningful Status Keys
Use descriptive status keys:
SetStatus("ReadyToTransfer", true); // ✅ Clear intent
SetStatus("Flag1", true); // ❌ Unclear meaning
5. Graceful Initialization
Handle initialization failures:
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;
}
});
}