Message Passing
Actors in ControlBee communicate through asynchronous messages. This decouples actors and enables flexible, traceable interactions.
Message Structure
A message contains a sender reference, message name, and optional data payload:
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; }
}
Sending Messages
Publishing Messages
Actors publish messages to themselves or peers:
// Send to self
Publish(new Message(this, "Start"));
// Send to peer
NextStage.Publish(new Message(this, "Transfer"));
// Send with simple data
Picker.Publish(new Message(this, "Pick", coordinates));
// Send with complex data using Dict
Picker.Publish(new Message(this, "Pick", new Dict
{
["X"] = 100.5,
["Y"] = 200.3,
["Z"] = 50.0
}));
Broadcasting
Send a message to multiple actors:
foreach (var peer in Peers)
{
peer.Publish(new Message(this, "Stop"));
}
Processing Messages
Messages are handled by the actor's current state:
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
}
}
}
Return value:
true- Message was handledfalse- Message not handled, try parent or default handler
Processing DictPayload
When messages contain Dict payloads, access them through 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;
}
}
Built-in Messages
ControlBee provides several system messages:
StateEntryMessage
Sent automatically when entering a state:
case StateEntryMessage.MessageName:
// Initialize state
Actor.SetStatus("Ready", true);
StartTimer(100); // Periodic scan
return true;
StateExitMessage
Sent automatically when leaving a state:
case StateExitMessage.MessageName:
// Clean up resources
StopTimer();
Actor.SetStatus("Ready", false);
return true;
TimerMessage
Sent periodically for scanning operations when configured in the actor constructor.
Configuration:
public FlipperActor(ActorConfig config) : base(config)
{
TimerMilliseconds = 200; // Enable timer with 200ms interval
// ... rest of initialization
}
Processing:
case TimerMessage.MessageName:
return Scan(); // Check sensors, update status
private bool Scan()
{
if (SensorChanged())
{
UpdateStatus();
return true;
}
return false;
}
Use Cases:
- Sensor polling and state monitoring
- Periodic status updates to other actors
- Timeout detection
- Background health checks
Status Communication
In addition to messages, actors communicate through status properties:
Setting Status
// Set local status
SetStatus("Busy", true);
SetStatus("Position", currentPos);
// Set status on another actor (via Syncer)
SetStatusByActor(Syncer, "ReadyToLoad", requestId);
Reading Status
// 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
}
Status vs. Messages
Use Status when:
- State is continuous (busy/idle, position, temperature)
- Multiple actors need to query the same information
- Latest value is important (not historical)
Use Messages when:
- Event is discrete (start, stop, error occurred)
- Action required (load, unload, initialize)
- Order matters (sequential operations)
Message Patterns
Request-Response Pattern
One actor requests, another responds:
// 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
}
Command Pattern
Direct command to perform action:
// 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 Pattern
Actors observe peer status changes:
// 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;
State Synchronization
Actors sync state periodically:
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;
}
Message Ordering
Messages are processed sequentially by each actor:
Actor Queue
┌─────────┐
│ Start │ ← Processed first
├─────────┤
│ Load │
├─────────┤
│ Unload │ ← Processed last
└─────────┘
This ensures:
- Consistent state - No race conditions
- Predictable behavior - Deterministic order
- Thread safety - No locking needed
Message Flow Example
Complete flow from sensor detection to action:
// 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;
}
}
Best Practices
1. Use Descriptive Message Names
Publish(new Message(this, "StartTransfer")); // ✅ Clear intent
Publish(new Message(this, "Msg1")); // ❌ Unclear
2. Handle All Messages
Return false for unhandled messages:
public override bool ProcessMessage(Message message)
{
switch (message.Name)
{
case "KnownMessage":
// Handle
return true;
default:
return false; // Let parent/default handler try
}
}
3. Avoid Blocking in Message Handlers
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. Use Status for Queries
// ❌ Avoid - requires message round-trip
Peer.Publish(new Message("GetPosition"));
// ... wait for response message ...
// ✅ Better - direct status read
int pos = (int)(GetPeerStatus(Peer, "Position") ?? 0);
5. Clean Up in StateExitMessage
case StateExitMessage.MessageName:
StopTimer();
CancelPendingOperations();
SetStatus("Busy", false);
return true;