Device Integration
ControlBee provides a factory-based abstraction layer for hardware devices, enabling hardware-independent actor development and easy device substitution.
Factory Pattern
Devices are created through factory interfaces, decoupling actors from specific hardware implementations:
public class StageActor : Actor
{
public IAxis X, Z;
public IDigitalOutput Vacuum;
public IDigitalInput MaterialSensor;
public StageActor(ActorConfig config) : base(config)
{
// Create devices through factories - actual hardware type determined at runtime
X = config.AxisFactory.Create();
Z = config.AxisFactory.Create();
Vacuum = config.DigitalOutputFactory.Create();
MaterialSensor = config.DigitalInputFactory.Create();
}
}
Benefits:
- Hardware Independence - Same actor code works with different hardware
- Easy Testing - Use simulated devices for testing
- Hot Swapping - Change hardware without modifying actor code
- Simulation Mode - Run without physical hardware
Device Types
Motion Control (IAxis)
Represents a single motion axis:
public class AxisActor : Actor
{
public IAxis X;
public AxisActor(ActorConfig config) : base(config)
{
X = config.AxisFactory.Create();
}
public override void Start()
{
base.Start();
// Enable the axis
X.Enable = true;
// Move to position
X.TrapezoidalMove(position: 100, speed: 50, accel: 100, decel: 100);
// Wait for completion
X.Wait();
// Check status
Console.WriteLine($"Position: {X.ActualPosition}");
Console.WriteLine($"Moving: {X.IsMoving}");
Console.WriteLine($"Error: {X.Error}");
}
}
Key Properties:
X.Enable = true; // Enable/disable axis
double pos = X.CommandPosition; // Commanded position
double actual = X.ActualPosition; // Actual position from encoder
bool moving = X.IsMoving; // Is axis currently moving?
bool error = X.Error; // Is axis in error state?
Key Methods:
// Trapezoidal move (non-blocking)
X.TrapezoidalMove(position: 100, speed: 50, accel: 100, decel: 100);
// Wait for move completion
X.Wait();
// Combined move and wait
X.TrapezoidalMoveAndWait(100, 50, 100, 100);
// Stop motion
X.Stop();
// Set position (homing)
X.SetCommandPosition(0);
Digital I/O
Digital Input (IDigitalInput)
Read binary sensor states:
public class SensorActor : Actor
{
public IDigitalInput MaterialSensor;
public IDigitalInput SafetyGate;
public SensorActor(ActorConfig config) : base(config)
{
MaterialSensor = config.DigitalInputFactory.Create();
SafetyGate = config.DigitalInputFactory.Create();
}
public void CheckSensors()
{
// Read sensor states
bool hasMaterial = MaterialSensor.Value;
bool gateOpen = SafetyGate.Value;
if (hasMaterial && !gateOpen)
{
StartProcessing();
}
}
}
Digital Output (IDigitalOutput)
Control binary actuators:
public class ActuatorActor : Actor
{
public IDigitalOutput Vacuum;
public IDigitalOutput Cylinder;
public IDigitalOutput StatusLight;
public ActuatorActor(ActorConfig config) : base(config)
{
Vacuum = config.DigitalOutputFactory.Create();
Cylinder = config.DigitalOutputFactory.Create();
StatusLight = config.DigitalOutputFactory.Create();
}
public void PickPart()
{
// Extend cylinder
Cylinder.Value = true;
Thread.Sleep(100); // Wait for position
// Turn on vacuum
Vacuum.Value = true;
Thread.Sleep(500); // Wait for vacuum
// Retract cylinder
Cylinder.Value = false;
// Indicate success
StatusLight.Value = true;
}
}
Convenience Methods:
Vacuum.TurnOn(); // Same as: Vacuum.Value = true
Vacuum.TurnOff(); // Same as: Vacuum.Value = false
bool state = Vacuum.Value;
Analog I/O
Analog Input (IAnalogInput)
Read continuous sensor values:
public class SensorActor : Actor
{
public IAnalogInput PressureSensor;
public IAnalogInput TemperatureSensor;
public SensorActor(ActorConfig config) : base(config)
{
PressureSensor = config.AnalogInputFactory.Create();
TemperatureSensor = config.AnalogInputFactory.Create();
}
public void MonitorConditions()
{
double pressure = PressureSensor.Value; // Read in engineering units
double temperature = TemperatureSensor.Value;
Console.WriteLine($"Pressure: {pressure:F2} kPa");
Console.WriteLine($"Temperature: {temperature:F1} °C");
if (pressure < -80 || temperature > 60)
{
TriggerAlarm();
}
}
}
Analog Output (IAnalogOutput)
Control continuous outputs:
public class ControlActor : Actor
{
public IAnalogOutput ValvePosition;
public IAnalogOutput HeaterPower;
public ControlActor(ActorConfig config) : base(config)
{
ValvePosition = config.AnalogOutputFactory.Create();
HeaterPower = config.AnalogOutputFactory.Create();
}
public void SetControlOutputs()
{
// Set valve to 50% open
ValvePosition.Value = 50.0;
// Set heater to 25% power
HeaterPower.Value = 25.0;
}
}
Vision System (IVision)
Image capture and processing:
public class VisionActor : Actor
{
public IVision Camera;
public VisionActor(ActorConfig config) : base(config)
{
Camera = config.VisionFactory.Create();
}
public VisionResult InspectPart()
{
// Trigger image capture
Camera.Trigger();
// Process image (implementation depends on vision system)
var result = Camera.GetResult();
Console.WriteLine($"X offset: {result.OffsetX}");
Console.WriteLine($"Y offset: {result.OffsetY}");
Console.WriteLine($"Angle: {result.Angle}");
Console.WriteLine($"Pass: {result.Pass}");
return result;
}
}
Device Configuration
Configuration Dictionary
Devices are configured using string dictionaries passed during creation:
// In device initialization code
var axisConfig = new Dictionary<string, string>
{
["AxisNumber"] = "0",
["PulsePerUnit"] = "1000",
["HomeDirection"] = "Negative",
["HomeSpeed"] = "10",
["SoftwareLimitPositive"] = "300",
["SoftwareLimitNegative"] = "-10"
};
var axis = axisFactory.Create();
axis.Init(axisConfig);
Common Configuration Parameters
Motion Axis:
AxisNumber- Physical axis indexPulsePerUnit- Encoder resolutionHomeDirection- Homing direction (Positive/Negative)HomeSpeed- Homing speedSoftwareLimitPositive- Upper limitSoftwareLimitNegative- Lower limit
Digital I/O:
DeviceIndex- Card/device indexChannelNumber- I/O channel numberInverted- Invert logic (true/false)
Analog I/O:
DeviceIndex- Card/device indexChannelNumber- I/O channel numberScale- Conversion factorOffset- Zero offsetMinValue- Minimum valueMaxValue- Maximum value
Device Lifecycle
1. Creation
Devices are created through factories:
public StageActor(ActorConfig config) : base(config)
{
// Factories provided by ActorConfig
X = config.AxisFactory.Create();
Vacuum = config.DigitalOutputFactory.Create();
}
2. Initialization
Devices are initialized by the framework with configuration:
// Framework calls Init() with configuration
axis.Init(configDictionary);
// After Init(), device is ready to use
axis.Enable = true;
3. Usage
Use device throughout actor lifetime:
public override void Start()
{
base.Start();
// Device is initialized and ready
X.Enable = true;
X.TrapezoidalMoveAndWait(100, 50, 100, 100);
}
4. Cleanup
Devices implement IDisposable for cleanup:
public void Dispose()
{
// Framework calls Dispose() when actor is destroyed
X?.Dispose();
Vacuum?.Dispose();
}
Creating Custom Devices
Device Driver Interface
All devices implement IDevice:
public interface IDevice : IDisposable
{
void Init(Dictionary<string, string> config);
}
Example: Custom Motion Device
public class MyMotionController : IMotionDevice
{
private int axisNumber;
private double pulsePerUnit;
private IntPtr cardHandle; // Hardware-specific
public bool Enable { get; set; }
public double CommandPosition { get; private set; }
public double ActualPosition { get; private set; }
public bool IsMoving { get; private set; }
public bool Error { get; private set; }
public void Init(Dictionary<string, string> config)
{
// Parse configuration
axisNumber = int.Parse(config["AxisNumber"]);
pulsePerUnit = double.Parse(config["PulsePerUnit"]);
// Initialize hardware
cardHandle = NativeApi.OpenCard();
NativeApi.ConfigureAxis(cardHandle, axisNumber);
}
public void TrapezoidalMove(double position, double speed, double accel, double decel)
{
// Convert to hardware units
int targetPulse = (int)(position * pulsePerUnit);
int speedPulse = (int)(speed * pulsePerUnit);
// Send command to hardware
NativeApi.Move(cardHandle, axisNumber, targetPulse, speedPulse);
CommandPosition = position;
IsMoving = true;
}
public void Wait()
{
// Poll until motion complete
while (IsMoving)
{
int status = NativeApi.GetStatus(cardHandle, axisNumber);
IsMoving = (status & 0x01) != 0;
// Update actual position
int pulse = NativeApi.GetPosition(cardHandle, axisNumber);
ActualPosition = pulse / pulsePerUnit;
Thread.Sleep(10);
}
}
public void Stop()
{
NativeApi.StopAxis(cardHandle, axisNumber);
IsMoving = false;
}
public void SetCommandPosition(double position)
{
int pulse = (int)(position * pulsePerUnit);
NativeApi.SetPosition(cardHandle, axisNumber, pulse);
CommandPosition = position;
}
public void Dispose()
{
if (cardHandle != IntPtr.Zero)
{
NativeApi.CloseCard(cardHandle);
cardHandle = IntPtr.Zero;
}
}
}
Example: Custom Digital I/O Device
public class MyDioCard : IDigitalIoDevice
{
private IntPtr cardHandle;
private int deviceIndex;
public void Init(Dictionary<string, string> config)
{
deviceIndex = int.Parse(config["DeviceIndex"]);
cardHandle = NativeApi.OpenDioCard(deviceIndex);
}
public bool GetDigitalInputBit(int channel)
{
return NativeApi.ReadInput(cardHandle, channel);
}
public void SetDigitalOutputBit(int channel, bool value)
{
NativeApi.WriteOutput(cardHandle, channel, value);
}
public void Dispose()
{
if (cardHandle != IntPtr.Zero)
{
NativeApi.CloseDioCard(cardHandle);
cardHandle = IntPtr.Zero;
}
}
}
Simulation Devices
ControlBee provides simulated devices for testing:
public class SimulatedAxis : IAxis
{
public bool Enable { get; set; }
public double CommandPosition { get; private set; }
public double ActualPosition { get; private set; }
public bool IsMoving { get; private set; }
public bool Error { get; private set; }
public void TrapezoidalMove(double position, double speed, double accel, double decel)
{
CommandPosition = position;
IsMoving = true;
// Simulate motion in background
Task.Run(() =>
{
Thread.Sleep(100); // Simulate move time
ActualPosition = position;
IsMoving = false;
});
}
public void Wait()
{
while (IsMoving)
{
Thread.Sleep(10);
}
}
// ... other methods
}
Usage:
// In initialization code, choose device based on mode
if (systemConfig.FakeMode)
{
axis = new SimulatedAxis();
}
else
{
axis = new RealMotionController();
}
Device Monitoring
Device Manager (IDeviceManager)
Central management of all hardware devices:
// Access through system services
var deviceManager = actorSystem.GetService<IDeviceManager>();
// Get all devices
var allDevices = deviceManager.GetAllDevices();
// Get devices by type
var motionDevices = deviceManager.GetDevicesByType<IMotionDevice>();
var dioDevices = deviceManager.GetDevicesByType<IDigitalIoDevice>();
// Monitor device status
foreach (var device in motionDevices)
{
if (device.Error)
{
Console.WriteLine($"Device {device} in error state");
}
}
Device Monitor (IDeviceMonitor)
Real-time monitoring of digital inputs:
public class MonitorActor : Actor
{
private IDeviceMonitor deviceMonitor;
public override void Start()
{
base.Start();
// Get device monitor service
deviceMonitor = GetService<IDeviceMonitor>();
// Monitor all digital inputs
deviceMonitor.InputChanged += OnInputChanged;
}
private void OnInputChanged(object sender, InputChangedEventArgs e)
{
Console.WriteLine($"Device {e.DeviceIndex}, Channel {e.Channel}: {e.Value}");
// React to input changes
if (e.Channel == 5 && e.Value)
{
HandleEmergencyStop();
}
}
}
Best Practices
1. Use Factories, Not Direct Instantiation
// ✅ Good - Uses factory
public StageActor(ActorConfig config) : base(config)
{
X = config.AxisFactory.Create();
}
// ❌ Bad - Direct instantiation
public StageActor(ActorConfig config) : base(config)
{
X = new SpecificMotionCard(); // Couples to specific hardware
}
2. Check Device Status
// ✅ Good - Checks for errors
X.TrapezoidalMoveAndWait(100, 50, 100, 100);
if (X.Error)
{
HandleMotionError();
}
// ❌ Bad - Assumes success
X.TrapezoidalMoveAndWait(100, 50, 100, 100);
// Continues without checking
3. Disable Axes When Not In Use
// ✅ Good - Disables when idle
public override void OnEntry(Message msg)
{
X.Enable = true;
}
public override void OnExit(Message msg)
{
X.Enable = false; // Disable when leaving state
}
// ❌ Bad - Always enabled
public override void OnEntry(Message msg)
{
X.Enable = true;
}
// Never disabled - wastes power, may overheat
4. Use Descriptive Device Names
// ✅ Good - Clear purpose
public IAxis LoaderX;
public IAxis LoaderZ;
public IDigitalOutput VacuumGripper;
public IDigitalInput PartDetectSensor;
// ❌ Bad - Unclear purpose
public IAxis Axis1;
public IAxis Axis2;
public IDigitalOutput DO1;
public IDigitalInput DI3;
5. Handle Hardware Initialization Errors
// ✅ Good - Validates initialization
public StageActor(ActorConfig config) : base(config)
{
try
{
X = config.AxisFactory.Create();
if (X == null)
{
throw new Exception("Failed to create X axis");
}
}
catch (Exception ex)
{
LogError($"Axis initialization failed: {ex.Message}");
// Set error flag or use simulated device
}
}
6. Dispose Properly
// ✅ Good - Disposes devices
public override void Dispose()
{
X?.Dispose();
Z?.Dispose();
Vacuum?.Dispose();
base.Dispose();
}
// ❌ Bad - Doesn't dispose
// Memory leaks, hardware resources not released
7. Use Simulation for Testing
// ✅ Good - Can run without hardware
if (SystemConfig.FakeMode)
{
Console.WriteLine("Running in simulation mode");
}
// Test actor logic without physical devices
X.TrapezoidalMoveAndWait(100, 50, 100, 100); // Works in both real and sim