본문으로 건너뛰기

장치 통합

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

참고