Skip to main content

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 handled
  • false - 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;

See Also