본문으로 건너뛰기

NuGet SCADA 독립 시스템 구축

이 튜토리얼에서는 Caffeine NuGet 패키지만으로 독립적인 SCADA(Supervisory Control and Data Acquisition) 시스템을 구축합니다. Caffeine Engine 설치 없이, NEXCODE.Caffeine.Scada 패키지로 장비를 모델링하고 실시간 데이터를 모니터링합니다.

개요​

단계주제소요 시간
1프로젝트 설정5분
2장비 모델 정의15분
3InterlockRule 안전 로직10분
4DI 등록 및 드라이버 연결10분
5실시간 모니터링10분
6AAS 연동 (선택)10분

사전 요구 사항:

학습 목표:

  • EquipmentNode 기반 장비 트리 구조를 설계할 수 있다
  • Sensor, Valve, Cylinder 컴포넌트로 I/O 포인트를 구성할 수 있다
  • InterlockRule로 안전 인터락을 구현할 수 있다
  • Caffeine Engine 없이 독립 SCADA를 실행할 수 있다

1단계: 프로젝트 설정​

Worker Service 프로젝트 생성​

dotnet new worker -n MyScadaSystem
cd MyScadaSystem

# Caffeine SCADA 패키지 추가
dotnet add package NEXCODE.Caffeine.Core
dotnet add package NEXCODE.Caffeine.Scada

# 선택: AAS 연동
dotnet add package NEXCODE.Caffeine.AAS

NuGet 패키지 역할​

패키지용도
NEXCODE.Caffeine.Core인터페이스, 엔티티, 값 객체
NEXCODE.Caffeine.Scada장비 모델, 센서, 밸브, 인터락
NEXCODE.Caffeine.AASAAS 디지털 트윈 연동 (선택)

2단계: 장비 모델 정의​

SCADA 장비 트리 구조​

Caffeine SCADA는 Module → Component → IoPart 계층 구조로 장비를 모델링합니다:

CvdEquipment.cs 정의​

using Caffeine.Scada.Equipment;
using Caffeine.Scada.Components;

namespace MyScadaSystem;

/// <summary>
/// CVD (Chemical Vapor Deposition) 반도체 장비.
/// 가스 공급 → 챔버 진공 → RF 플라즈마 → 박막 증착 공정.
/// </summary>
public class CvdEquipment : EquipmentNode
{
// 모듈
public ModuleNode GasModule { get; }
public ModuleNode ChamberModule { get; }
public ModuleNode RFModule { get; }

// 가스 밸브
public Valve N2Valve { get; }
public Valve SiH4Valve { get; }

// 챔버 컴포넌트
public Cylinder GateValve { get; }
public AnalogSensor TemperatureSensor { get; }
public AnalogSensor PressureSensor { get; }

// RF 전원
public AnalogSensor RFPowerSensor { get; }

public CvdEquipment(string name) : base(name)
{
// 1. 모듈 생성
GasModule = AddModule("Gas");
ChamberModule = AddModule("Chamber");
RFModule = AddModule("RF");

// 2. 가스 모듈 — 밸브 2개
N2Valve = GasModule.AddComponent(
new Valve("N2", "질소 가스 밸브"));
SiH4Valve = GasModule.AddComponent(
new Valve("SiH4", "실란 가스 밸브"));

// 3. 챔버 모듈 — 게이트 밸브 + 센서
GateValve = ChamberModule.AddComponent(
new Cylinder("GateValve", "챔버 게이트 밸브"));
TemperatureSensor = ChamberModule.AddComponent(
new AnalogSensor("Temperature", "챔버 온도", "°C",
minValue: 0, maxValue: 800));
PressureSensor = ChamberModule.AddComponent(
new AnalogSensor("Pressure", "챔버 압력", "mTorr",
minValue: 0, maxValue: 1000));

// 4. RF 모듈 — 전력 센서
RFPowerSensor = RFModule.AddComponent(
new AnalogSensor("RFPower", "RF 전력", "W",
minValue: 0, maxValue: 5000));
}
}

전체 I/O 포인트 확인​

var cvd = new CvdEquipment("CVD-001");

// 장비의 모든 I/O 포인트 열거
foreach (var part in cvd.EnumerateAllParts())
{
Console.WriteLine($" {part.FullPath}: {part.GetType().Name}");
}
// 출력:
// CVD-001.Gas.N2: Valve
// CVD-001.Gas.SiH4: Valve
// CVD-001.Chamber.GateValve: Cylinder
// CVD-001.Chamber.Temperature: AnalogSensor
// CVD-001.Chamber.Pressure: AnalogSensor
// CVD-001.RF.RFPower: AnalogSensor

3단계: InterlockRule 안전 로직​

**인터락(Interlock)**은 장비 안전을 위한 조건부 동작 차단 규칙입니다. 예: "챔버가 진공 상태가 아니면 RF 전원을 투입할 수 없다"

인터락 규칙 정의​

// CvdEquipment 생성자 내부에 추가

// 규칙 1: RF Power 투입 전 챔버 Vacuum 확인
RFPowerSensor.AddInterlock(new InterlockRule(
name: "VacuumCheck",
description: "RF 전원 투입 전 챔버 압력이 100mTorr 이하여야 합니다",
condition: () => PressureSensor.CurrentValue <= 100.0,
severity: InterlockSeverity.Critical
));

// 규칙 2: Gate Valve Open 전 N2 Purge 완료 확인
GateValve.AddInterlock(new InterlockRule(
name: "N2PurgeCheck",
description: "게이트 밸브 개방 전 N2 퍼지가 완료되어야 합니다",
condition: () => N2Valve.IsOpen,
severity: InterlockSeverity.Warning
));

// 규칙 3: SiH4 가스 투입 전 챔버 온도 확인
SiH4Valve.AddInterlock(new InterlockRule(
name: "TemperatureCheck",
description: "실란 가스 투입 전 챔버 온도가 200°C 이상이어야 합니다",
condition: () => TemperatureSensor.CurrentValue >= 200.0,
severity: InterlockSeverity.Critical
));

인터락 검증​

// 인터락 위반 시 동작 차단
var result = SiH4Valve.TryOpen();
if (!result.Success)
{
Console.WriteLine($"차단됨: {result.ViolatedRule?.Description}");
// 출력: 차단됨: 실란 가스 투입 전 챔버 온도가 200°C 이상이어야 합니다
}

4단계: DI 등록 및 드라이버 연결​

경로 1: 코드에서 직접 등록​

// Program.cs
using MyScadaSystem;

var builder = Host.CreateApplicationBuilder(args);

var cvd = new CvdEquipment("CVD-001");

builder.Services.AddCaffeineScada();
builder.Services.AddCaffeineEquipment(cvd, driver =>
{
driver.UseSimulation(); // 개발 단계: 시뮬레이션 드라이버
// driver.Use("ModbusTcp"); // 프로덕션: 실제 드라이버
});

builder.Services.AddHostedService<MonitoringWorker>();

var host = builder.Build();
host.Run();

경로 2: JSON Config 기반 Fleet 관리​

다수의 장비를 설정 파일로 관리할 수 있습니다:

appsettings.json:

{
"EquipmentFleet": {
"EquipmentType": "CvdEquipment",
"Driver": {
"Type": "ModbusTcp",
"Port": 502,
"AddressMapping": "Sequential"
},
"DefaultProfile": {
"MaxTemperature": 800,
"MaxPressure": 1000
},
"Instances": [
{
"Name": "CVD-001",
"Ip": "192.168.1.101",
"BaseAddress": "D100"
},
{
"Name": "CVD-002",
"Ip": "192.168.1.102",
"BaseAddress": "D100"
},
{
"Name": "CVD-003",
"Ip": "192.168.1.103",
"BaseAddress": "D200",
"Profile": {
"MaxTemperature": 600
}
}
]
}
}

Program.cs (Fleet 방식):

builder.Services.AddCaffeineScada();
builder.Services.AddEquipmentFleetFromConfig<CvdEquipment>(
builder.Configuration.GetSection("EquipmentFleet"));

5단계: 실시간 모니터링​

MonitoringWorker.cs​

using Caffeine.Scada.Equipment;
using System.Reactive.Linq;

namespace MyScadaSystem;

public class MonitoringWorker : BackgroundService
{
private readonly IEnumerable<EquipmentNode> _equipments;
private readonly ILogger<MonitoringWorker> _logger;

public MonitoringWorker(
IEnumerable<EquipmentNode> equipments,
ILogger<MonitoringWorker> logger)
{
_equipments = equipments;
_logger = logger;
}

protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
foreach (var equipment in _equipments)
{
if (equipment is CvdEquipment cvd)
{
SubscribeSensors(cvd, stoppingToken);
}
}

return Task.CompletedTask;
}

private void SubscribeSensors(CvdEquipment cvd, CancellationToken ct)
{
// 온도 센서 — 1초마다 최신 값
cvd.TemperatureSensor.ValueStream
.Throttle(TimeSpan.FromSeconds(1))
.Subscribe(
value => _logger.LogInformation(
"[{Equipment}] 온도: {Temp:F1}°C",
cvd.Name, value),
ct);

// 압력 센서 — 임계값 초과 시 경고
cvd.PressureSensor.ValueStream
.Where(pressure => pressure > 800)
.Subscribe(
value => _logger.LogWarning(
"[{Equipment}] ⚠️ 압력 경고: {Pressure:F0} mTorr",
cvd.Name, value),
ct);

// 모든 센서 — 10초 평균
cvd.TemperatureSensor.ValueStream
.Buffer(TimeSpan.FromSeconds(10))
.Where(buffer => buffer.Count > 0)
.Subscribe(buffer =>
{
var avg = buffer.Average();
_logger.LogInformation(
"[{Equipment}] 10초 평균 온도: {Avg:F1}°C",
cvd.Name, avg);
}, ct);
}
}

실행​

dotnet run

# 출력 예시:
# [INF] [CVD-001] 온도: 425.3°C
# [INF] [CVD-001] 10초 평균 온도: 423.7°C
# [WRN] [CVD-001] ⚠️ 압력 경고: 852 mTorr

6단계: AAS 연동 (선택)​

장비 모델을 IEC 63278 AAS(Asset Administration Shell)로 자동 변환하여 디지털 트윈으로 관리할 수 있습니다.

AAS 모듈 추가​

// Program.cs에 추가
builder.Services.AddCaffeineAas(aas =>
{
aas.AddSubmodelProvider<NameplateSubmodelProvider>()
.AddSubmodelProvider<OperationalDataSubmodelProvider>()
.WithAasxPackageSupport();
});

장비 → AAS Shell 변환​

// 장비 모델에서 AAS Shell 자동 생성
app.MapGet("/aas/shells", async (
IEnumerable<EquipmentNode> equipments,
TagToAasConverter converter) =>
{
var shells = new List<object>();

foreach (var eq in equipments)
{
// 장비의 모든 I/O 파트를 AAS SubmodelElement로 변환
var elements = eq.EnumerateAllParts()
.Select(part => converter.Convert(part));

shells.Add(new
{
IdShort = eq.Name,
GlobalAssetId = $"urn:nexcode:scada:{eq.Name}",
Elements = elements.Count()
});
}

return shells;
});

AASX 내보내기​

app.MapGet("/aas/export/{equipmentName}", async (
string equipmentName,
AasxPackageHandler packageHandler) =>
{
var bytes = await packageHandler.ExportAsync(equipmentName);
return Results.File(bytes,
"application/asset-administration-shell-package",
$"{equipmentName}.aasx");
});

배운 내용 정리​

항목설명
EquipmentNode장비 트리의 루트 — Module/Component/IoPart 계층
ModuleNode기능별 그룹 (Gas, Chamber, RF 등)
Sensor, Valve, CylinderI/O 컴포넌트 — 실시간 값 스트림 제공
InterlockRule안전 조건부 동작 차단 규칙
AddCaffeineScada()SCADA 기본 서비스 DI 등록
AddCaffeineEquipment<T>()단일 장비 등록 (코드 방식)
AddEquipmentFleetFromConfig()다수 장비 등록 (JSON Config)
ValueStreamIObservable<double> — Rx.NET 실시간 스트림

다음 단계​


문제 해결​

Q: AddCaffeineScada()가 인식되지 않습니다.

NEXCODE.Caffeine.Scada NuGet 패키지가 설치되어 있는지 확인하세요. using Caffeine.Scada.Extensions;를 추가해야 합니다.

Q: 시뮬레이션 드라이버에서 값이 변하지 않습니다.

EquipmentDriverBuilder.UseSimulation() 호출 시 자동으로 랜덤 데이터가 생성됩니다. ValueStream 구독이 정상적으로 설정되었는지 확인하세요.

Q: InterlockRule 조건이 항상 false입니다.

인터락 조건 함수(condition)는 매 호출 시 평가됩니다. CurrentValue가 업데이트되기 전에 체크하면 초기값(0)으로 평가됩니다. 센서 값이 수신된 후 인터락을 확인하세요.

Q: Fleet Config에서 장비가 생성되지 않습니다.

EquipmentType이 실제 클래스명과 일치하는지 확인하세요. 네임스페이스 없이 클래스명만 지정합니다 (예: "CvdEquipment").