본문으로 건너뛰기

장치 통합

ControlBee는 하드웨어 장치에 대한 팩토리 기반 추상화 계층을 제공하여 하드웨어 독립적인 액터 개발과 쉬운 장치 교체를 가능하게 합니다.

팩토리 패턴​

장치는 팩토리 인터페이스를 통해 생성되어 액터를 특정 하드웨어 구현에서 분리합니다:

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();
}
}

이점:

  • 하드웨어 독립성 - 동일한 액터 코드가 다른 하드웨어에서 작동합니다
  • 쉬운 테스트 - 테스트를 위해 시뮬레이션 장치를 사용합니다
  • 핫 스왑 - 액터 코드를 수정하지 않고 하드웨어를 변경합니다
  • 시뮬레이션 모드 - 물리적 하드웨어 없이 실행합니다

장치 타입​

모션 제어 (IAxis)​

단일 모션 축을 나타냅니다:

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}");
}
}

주요 속성:

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?

주요 메서드:

// 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);

디지털 I/O​

디지털 입력 (IDigitalInput)​

바이너리 센서 상태를 읽습니다:

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();
}
}
}

디지털 출력 (IDigitalOutput)​

바이너리 액추에이터를 제어합니다:

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;
}
}

편의 메서드:

Vacuum.TurnOn();          // Same as: Vacuum.Value = true
Vacuum.TurnOff(); // Same as: Vacuum.Value = false
bool state = Vacuum.Value;

아날로그 I/O​

아날로그 입력 (IAnalogInput)​

연속 센서 값을 읽습니다:

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();
}
}
}

아날로그 출력 (IAnalogOutput)​

연속 출력을 제어합니다:

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;
}
}

비전 시스템 (IVision)​

이미지 캡처 및 처리:

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;
}
}

장치 구성​

구성 딕셔너리​

장치는 생성 중에 전달되는 문자열 딕셔너리를 사용하여 구성됩니다:

// 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);

일반 구성 매개변수​

모션 축:

  • AxisNumber - 물리적 축 인덱스
  • PulsePerUnit - 엔코더 해상도
  • HomeDirection - 원점 복귀 방향 (Positive/Negative)
  • HomeSpeed - 원점 복귀 속도
  • SoftwareLimitPositive - 상한
  • SoftwareLimitNegative - 하한

디지털 I/O:

  • DeviceIndex - 카드/장치 인덱스
  • ChannelNumber - I/O 채널 번호
  • Inverted - 논리 반전 (true/false)

아날로그 I/O:

  • DeviceIndex - 카드/장치 인덱스
  • ChannelNumber - I/O 채널 번호
  • Scale - 변환 계수
  • Offset - 제로 오프셋
  • MinValue - 최소값
  • MaxValue - 최대값

장치 수명 주기​

1. 생성​

장치는 팩토리를 통해 생성됩니다:

public StageActor(ActorConfig config) : base(config)
{
// Factories provided by ActorConfig
X = config.AxisFactory.Create();
Vacuum = config.DigitalOutputFactory.Create();
}

2. 초기화​

장치는 프레임워크에 의해 구성으로 초기화됩니다:

// Framework calls Init() with configuration
axis.Init(configDictionary);

// After Init(), device is ready to use
axis.Enable = true;

3. 사용​

액터 수명 내내 장치를 사용합니다:

public override void Start()
{
base.Start();

// Device is initialized and ready
X.Enable = true;
X.TrapezoidalMoveAndWait(100, 50, 100, 100);
}

4. 정리​

장치는 정리를 위해 IDisposable을 구현합니다:

public void Dispose()
{
// Framework calls Dispose() when actor is destroyed
X?.Dispose();
Vacuum?.Dispose();
}

사용자 정의 장치 생성​

장치 드라이버 인터페이스​

모든 장치는 IDevice를 구현합니다:

public interface IDevice : IDisposable
{
void Init(Dictionary<string, string> config);
}

예제: 사용자 정의 모션 장치​

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;
}
}
}

예제: 사용자 정의 디지털 I/O 장치​

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;
}
}
}

시뮬레이션 장치​

ControlBee는 테스트를 위한 시뮬레이션 장치를 제공합니다:

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
}

사용법:

// In initialization code, choose device based on mode
if (systemConfig.FakeMode)
{
axis = new SimulatedAxis();
}
else
{
axis = new RealMotionController();
}

장치 모니터링​

장치 관리자 (IDeviceManager)​

모든 하드웨어 장치의 중앙 관리:

// 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");
}
}

장치 모니터 (IDeviceMonitor)​

디지털 입력의 실시간 모니터링:

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();
}
}
}

모범 사례​

1. 팩토리 사용, 직접 인스턴스화 금지​

// ✅ 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. 장치 상태 확인​

// ✅ 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. 사용하지 않을 때 축 비활성화​

// ✅ 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. 설명적인 장치 이름 사용​

// ✅ 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. 하드웨어 초기화 오류 처리​

// ✅ 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. 올바르게 해제​

// ✅ 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. 테스트를 위한 시뮬레이션 사용​

// ✅ 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

참고​