Skip to main content

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

See Also