Skip to main content

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 index
  • PulsePerUnit - Encoder resolution
  • HomeDirection - Homing direction (Positive/Negative)
  • HomeSpeed - Homing speed
  • SoftwareLimitPositive - Upper limit
  • SoftwareLimitNegative - Lower limit

Digital I/O:

  • DeviceIndex - Card/device index
  • ChannelNumber - I/O channel number
  • Inverted - Invert logic (true/false)

Analog I/O:

  • DeviceIndex - Card/device index
  • ChannelNumber - I/O channel number
  • Scale - Conversion factor
  • Offset - Zero offset
  • MinValue - Minimum value
  • MaxValue - 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

See Also