NuGet SCADA 독립 시스템 구축
이 튜토리얼에서는 Caffeine NuGet 패키지만으로 독립적인 SCADA(Supervisory Control and Data Acquisition) 시스템을 구축합니다.
Caffeine Engine 설치 없이, NEXCODE.Caffeine.Scada 패키지로 장비를 모델링하고 실시간 데이터를 모니터링합니다.
개요
| 단계 | 주제 | 소요 시간 |
|---|---|---|
| 1 | 프로젝트 설정 | 5분 |
| 2 | 장비 모델 정의 | 15분 |
| 3 | InterlockRule 안전 로직 | 10분 |
| 4 | DI 등록 및 드라이버 연결 | 10분 |
| 5 | 실시간 모니터링 | 10분 |
| 6 | AAS 연동 (선택) | 10분 |
사전 요구 사항:
- Tutorial 02: 첫 번째 드라이버 완료
- .NET 10 SDK
- C# 중급 수준
- Rx.NET 기본 지식 (IObservable/Subscribe)
학습 목표:
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.AAS | AAS 디지털 트윈 연동 (선택) |
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, Cylinder | I/O 컴포넌트 — 실시간 값 스트림 제공 |
InterlockRule | 안전 조건부 동작 차단 규칙 |
AddCaffeineScada() | SCADA 기본 서비스 DI 등록 |
AddCaffeineEquipment<T>() | 단일 장비 등록 (코드 방식) |
AddEquipmentFleetFromConfig() | 다수 장비 등록 (JSON Config) |
ValueStream | IObservable<double> — Rx.NET 실시간 스트림 |
다음 단계
- AAS 심화 튜토리얼 — Custom Provider 개발
- Raspberry Pi 배포 — 이 SCADA를 엣지에 배포
- 프로덕션 배포 — Kubernetes 오케스트레이션
문제 해결
Q: AddCaffeineScada()가 인식되지 않습니다.
NEXCODE.Caffeine.ScadaNuGet 패키지가 설치되어 있는지 확인하세요.using Caffeine.Scada.Extensions;를 추가해야 합니다.
Q: 시뮬레이션 드라이버에서 값이 변하지 않습니다.
EquipmentDriverBuilder.UseSimulation()호출 시 자동으로 랜덤 데이터가 생성됩니다.ValueStream구독이 정상적으로 설정되었는지 확인하세요.
Q: InterlockRule 조건이 항상 false입니다.
인터락 조건 함수(
condition)는 매 호출 시 평가됩니다.CurrentValue가 업데이트되기 전에 체크하면 초기값(0)으로 평가됩니다. 센서 값이 수신된 후 인터락을 확인하세요.
Q: Fleet Config에서 장비가 생성되지 않습니다.
EquipmentType이 실제 클래스명과 일치하는지 확인하세요. 네임스페이스 없이 클래스명만 지정합니다 (예:"CvdEquipment").