Variable System
ControlBee's Variable System provides typed, scope-aware state management with persistence and change tracking.
Variable Declaration
Variables are strongly-typed with three scope levels:
public class StageActor : Actor
{
// Global - Shared across all recipes, auto-saved
public Variable<double> SafetyMargin = new(VariableScope.Global, 5.0);
public Variable<int> HomeSpeed = new(VariableScope.Global, 10);
// Local - Different value per recipe, auto-saved
public Variable<double> LoadSpeed = new(VariableScope.Local, 100.0);
public Variable<Position1D> LoadPosX = new(VariableScope.Local);
// Temporary - Runtime state, not auto-saved
public Variable<bool> Busy = new(VariableScope.Temporary);
public Variable<GridContainer> Tray = new(VariableScope.Temporary);
}
Variable Scopes
Global Scope
Purpose: Configuration shared across all recipes
Characteristics:
- Shared across all recipes (same value for all products)
- Auto-saved when values change (if AutoVariableSave is enabled)
- Subject to
DiscardChanges()to revert unsaved changes - Configurable through UI
- Persists across application restarts
When to Use:
- Machine-level configuration (safety margins, home speeds)
- System-wide parameters (communication timeouts, retry counts)
- Common settings that don't vary by product
- Any parameter that should be the same regardless of current recipe
Examples:
// Machine-level settings
public Variable<double> SafetyMargin = new(VariableScope.Global, 5.0);
public Variable<int> HomeSpeed = new(VariableScope.Global, 10);
// System-wide parameters
public Variable<int> CommunicationTimeout = new(VariableScope.Global, 5000);
public Variable<int> MaxRetryCount = new(VariableScope.Global, 3);
// Common configurations
public Variable<string> MachineName = new(VariableScope.Global, "Machine1");
public Variable<bool> EnableDebugMode = new(VariableScope.Global, false);
Local Scope
Purpose: Recipe-specific parameters that vary per product
Characteristics:
- Different value for each recipe (Recipe1, Recipe2, etc.)
- Auto-saved when values change (if AutoVariableSave is enabled)
- Loaded when switching recipes via
Load(recipeName) - Subject to
DiscardChanges()to revert unsaved changes - Configurable through UI
When to Use:
- Position variables that differ per product
- Process parameters per recipe
- Product-specific dimensions or offsets
- Any parameter that needs different values for different recipes
Examples:
// Product-specific positions
public Variable<double> LoadSpeed = new(VariableScope.Local, 100.0);
public Variable<Position1D> LoadPosX = new(VariableScope.Local);
public Variable<Position3D> PickPosXYZ = new(VariableScope.Local);
// Recipe-specific process parameters
public Variable<int> CycleCount = new(VariableScope.Local, 10);
public Variable<double> Temperature = new(VariableScope.Local, 25.0);
// Product dimensions
public Variable<int> RowCount = new(VariableScope.Local, 5);
public Variable<int> ColCount = new(VariableScope.Local, 8);
Temporary Scope
Purpose: Temporary runtime state
Characteristics:
- NOT auto-saved - must call
SaveTemporaryVariables()explicitly - NOT affected by
DiscardChanges()or recipe switching - Persists across application restarts (if manually saved)
- Used for temporary execution state
When to Use:
- Runtime execution state (Busy, Error flags)
- Temporary work tracking
- Transient data that doesn't need automatic persistence
- State that should not trigger automatic save operations
Examples:
// Runtime state
public Variable<bool> Busy = new(VariableScope.Temporary);
public Variable<bool> Exists = new(VariableScope.Temporary);
// Temporary work tracking
public Variable<GridContainer> Tray = new(VariableScope.Temporary);
public Variable<int> CurrentIndex = new(VariableScope.Temporary);
Supported Types
Scalar Types
public Variable<int> Count = new(VariableScope.Local, 0);
public Variable<double> Value = new(VariableScope.Local, 0.0);
public Variable<bool> Flag = new(VariableScope.Local, false);
public Variable<string> Name = new(VariableScope.Local, "");
Position Types
// 1D position (mapped to single axis)
public Variable<Position1D> TargetX = new(VariableScope.Local);
// Multi-axis positions
public Variable<Position2D> TargetXY = new(VariableScope.Local);
public Variable<Position3D> TargetXYZ = new(VariableScope.Local);
Array Types
// 1D arrays
public Variable<Array1D<bool>> Flags =
new(VariableScope.Local, new Array1D<bool>([true, false, true]));
public Variable<Array1D<Position1D>> Positions =
new(VariableScope.Local, new Array1D<Position1D>(5));
// 2D arrays (grids)
public Variable<Array2D<bool>> Grid =
new(VariableScope.Local, new Array2D<bool>(5, 8));
Custom Container Types
// Complex state container
public Variable<GridContainer> Tray = new(VariableScope.Temporary);
public class GridContainer : PropertyVariable
{
public Array2D<bool> CellExists { get; set; }
public Array2D<bool> WorkDone { get; set; }
public Array2D<VisionResult> CellVisionResult { get; set; }
public GridContainer(int rows, int cols)
{
CellExists = new Array2D<bool>(rows, cols);
WorkDone = new Array2D<bool>(rows, cols);
CellVisionResult = new Array2D<VisionResult>(rows, cols);
}
}
Variable Operations
Getting and Setting Values
// Get value
double currentSpeed = Speed.Value;
bool hasMaterial = Exists.Value;
// Set value
Speed.Value = 150.0;
Exists.Value = true;
// Array access
bool cellDone = WorkGrid.Value[row, col];
WorkGrid.Value[row, col] = true;
Change Notifications
Subscribe to variable changes:
public StageActor(ActorConfig config) : base(config)
{
// Subscribe to changes
Tray.ValueChanged += OnTrayChanged;
RowCount.ValueChanged += OnLayoutChanged;
}
private void OnTrayChanged(object? sender, ValueChangedArgs e)
{
var oldTray = (GridContainer)e.OldValue!;
var newTray = (GridContainer)e.NewValue!;
Console.WriteLine($"Tray changed from {oldTray.RowCount}x{oldTray.ColCount} " +
$"to {newTray.RowCount}x{newTray.ColCount}");
UpdateStatus();
}
private void OnLayoutChanged(object? sender, ValueChangedArgs e)
{
int oldCount = (int)e.OldValue!;
int newCount = (int)e.NewValue!;
// Reinitialize grid
Tray.Value = new GridContainer(newCount, ColCount.Value);
}
Position-Axis Mapping
Position variables map to physical axes:
public class StageActor : Actor
{
public IAxis X, Z;
public Variable<Position1D> LoadPosX = new(VariableScope.Local);
public Variable<Position1D> UnloadPosX = new(VariableScope.Local);
public Variable<Position1D> LoadPosZ = new(VariableScope.Local);
public StageActor(ActorConfig config) : base(config)
{
X = config.AxisFactory.Create();
Z = config.AxisFactory.Create();
// Map positions to axes
PositionAxesMap.Add(LoadPosX, [X]);
PositionAxesMap.Add(UnloadPosX, [X]);
PositionAxesMap.Add(LoadPosZ, [Z]);
}
}
// Move using position variable
LoadPosX.Value.MoveAndWait(); // Moves X axis
UnloadPosX.Value.MoveAndWait(); // Moves same X axis to different position
LoadPosZ.Value.MoveAndWait(); // Moves Z axis
Variable Persistence
Saving Variables
// Save all Global variables for current recipe
variableManager.Save();
// Save to specific recipe
variableManager.Save("Recipe1");
Loading Variables
// Load variables for current recipe
variableManager.Load();
// Load specific recipe
variableManager.Load("Recipe1");
// Event notification
variableManager.LoadCompleted += (sender, recipeName) =>
{
Console.WriteLine($"Loaded recipe: {recipeName}");
};
Recipe Management
// Get available recipes
string[] recipes = variableManager.LocalNames;
// Delete a recipe
variableManager.Delete("OldRecipe");
// Rename a recipe
variableManager.RenameLocalName("Recipe1", "ProductA");
Variable Change History
Track changes for auditing:
// Enable change tracking (automatic for Global variables)
variable.Dirty = true; // Marks as changed
// Query change history
var changes = variableManager.ReadVariableChanges(new QueryOptions
{
Filter = "ActorName = 'Stage0'",
OrderBy = "Timestamp DESC",
Limit = 100
});
// Columns: Timestamp, ActorName, ItemPath, OldValue, NewValue, UserName
Variable Visibility
Control UI visibility dynamically:
public class StageActor : Actor
{
public Variable<bool> UseVacuum = new(VariableScope.Local, true);
public Variable<double> VacuumPressure = new(VariableScope.Local, -80.0);
public override void Start()
{
base.Start();
// Hide vacuum parameters if not used
((IActorItemModifier)VacuumPressure).Visible = UseVacuum.Value;
// React to changes
UseVacuum.ValueChanged += (s, e) =>
{
((IActorItemModifier)VacuumPressure).Visible = (bool)e.NewValue!;
};
}
}
Best Practices
1. Choose the Right Scope
// ✅ Global - User needs to configure
public Variable<double> Speed = new(VariableScope.Local, 100.0);
// ✅ Local - Fixed offset, no persistence needed
public Variable<double> SafetyMargin = new(VariableScope.Local, 5.0);
// ✅ Temporary - Execution state
public Variable<bool> Busy = new(VariableScope.Temporary);
// ❌ Wrong - Runtime state as Global (unnecessary persistence)
public Variable<bool> Busy = new(VariableScope.Local, false);
2. Provide Meaningful Defaults
// ✅ Sensible default
public Variable<double> Speed = new(VariableScope.Local, 100.0);
// ❌ Missing default (confusing for users)
public Variable<double> Speed = new(VariableScope.Local);
3. Use Descriptive Names
public Variable<Position1D> LoadPosition = new(VariableScope.Local); // ✅
public Variable<Position1D> Pos1 = new(VariableScope.Local); // ❌
4. Validate Changes
Speed.ValueChanged += (s, e) =>
{
double newSpeed = (double)e.NewValue!;
if (newSpeed < 0 || newSpeed > 200)
{
Speed.Value = (double)e.OldValue!; // Revert
ErrorDialog.Show("Speed must be between 0 and 200");
}
};
5. Group Related Variables
// ✅ Clear grouping
public Variable<Position1D> LoadPosX = new(VariableScope.Local);
public Variable<Position1D> LoadPosZ = new(VariableScope.Local);
public Variable<Position1D> UnloadPosX = new(VariableScope.Local);
public Variable<Position1D> UnloadPosZ = new(VariableScope.Local);