첫 번째 드라이버 만들기
Caffeine Framework로 커스텀 드라이버를 개발하는 방법을 배웁니다.
🎯 학습 목표
IDriver인터페이스 구현- 태그 읽기/쓰기 로직 작성
- 연결 관리 및 에러 처리
- 드라이버 테스트
예상 소요 시간: 30분
📋 사전 요구사항
- Hello Caffeine 튜토리얼 완료
- .NET 10.0 SDK
- Caffeine CLI
Step 1: 드라이버 프로젝트 생성
# 드라이버 프로젝트 생성 (테스트 포함)
cafe init --name MyCustomDriver --template driver-full --git
cd MyCustomDriver
생성된 구조:
MyCustomDriver/
├── src/
│ ├── MyCustomDriver.csproj
│ └── MyCustomDriver.cs
├── tests/
│ ├── MyCustomDriver.Tests.csproj
│ └── MyCustomDriverTests.cs
└── .gitignore
Step 2: IDriver 인터페이스 구현
src/MyCustomDriver.cs를 수정합니다:
using Caffeine.Core.Abstractions;
using Caffeine.Core.Models;
using Microsoft.Extensions.Logging;
namespace MyCustomDriver;
public class MyCustomDriver : IDriver
{
private readonly ILogger<MyCustomDriver> _logger;
private readonly Dictionary<string, object> _tagValues = new();
private bool _isConnected;
public MyCustomDriver(ILogger<MyCustomDriver> logger)
{
_logger = logger;
}
public async Task<bool> ConnectAsync(CancellationToken cancellationToken = default)
{
_logger.LogInformation("장비 연결 시작...");
try
{
// 실제 장비 연결 로직
// 예: TCP 소켓 연결, 시리얼 포트 열기 등
await Task.Delay(100, cancellationToken); // 시뮬레이션
_isConnected = true;
_logger.LogInformation("✅ 장비 연결 성공");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "❌ 장비 연결 실패");
return false;
}
}
public async Task DisconnectAsync()
{
_logger.LogInformation("장비 연결 해제 중...");
// 실제 연결 해제 로직
await Task.Delay(50);
_isConnected = false;
_logger.LogInformation("✅ 연결 해제 완료");
}
public async Task<TagValue?> ReadTagAsync(
string tagName,
CancellationToken cancellationToken = default)
{
if (!_isConnected)
{
throw new InvalidOperationException("장비가 연결되지 않았습니다");
}
_logger.LogDebug("태그 읽기: {TagName}", tagName);
try
{
// 실제 장비에서 데이터 읽기
var value = await ReadFromEquipmentAsync(tagName, cancellationToken);
return new TagValue(tagName, value, TagQuality.Good);
}
catch (Exception ex)
{
_logger.LogError(ex, "태그 읽기 실패: {TagName}", tagName);
return new TagValue(tagName, 0, TagQuality.Bad);
}
}
public async Task<bool> WriteTagAsync(
string tagName,
object value,
CancellationToken cancellationToken = default)
{
if (!_isConnected)
{
throw new InvalidOperationException("장비가 연결되지 않았습니다");
}
_logger.LogDebug("태그 쓰기: {TagName} = {Value}", tagName, value);
try
{
// 실제 장비에 데이터 쓰기
await WriteToEquipmentAsync(tagName, value, cancellationToken);
_logger.LogInformation("✅ 태그 쓰기 성공: {TagName}", tagName);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "❌ 태그 쓰기 실패: {TagName}", tagName);
return false;
}
}
// 실제 장비 통신 메서드 (구현 필요)
private async Task<object> ReadFromEquipmentAsync(
string tagName,
CancellationToken cancellationToken)
{
// 시뮬레이션: 랜덤 값 반환
await Task.Delay(10, cancellationToken);
return tagName switch
{
"Temperature" => Random.Shared.Next(20, 80),
"Pressure" => Random.Shared.Next(90, 110),
"Flow" => Random.Shared.Next(30, 60),
_ => 0
};
}
private async Task WriteToEquipmentAsync(
string tagName,
object value,
CancellationToken cancellationToken)
{
// 시뮬레이션: 값 저장
await Task.Delay(10, cancellationToken);
_tagValues[tagName] = value;
}
}
Step 3: 단위 테스트 작성
tests/MyCustomDriverTests.cs:
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace MyCustomDriver.Tests;
public class MyCustomDriverTests
{
private readonly MyCustomDriver _driver;
public MyCustomDriverTests()
{
_driver = new MyCustomDriver(NullLogger<MyCustomDriver>.Instance);
}
[Fact]
public async Task ConnectAsync_ShouldReturnTrue()
{
// Act
var result = await _driver.ConnectAsync();
// Assert
Assert.True(result);
}
[Fact]
public async Task ReadTagAsync_WhenConnected_ShouldReturnValue()
{
// Arrange
await _driver.ConnectAsync();
// Act
var tagValue = await _driver.ReadTagAsync("Temperature");
// Assert
Assert.NotNull(tagValue);
Assert.Equal("Temperature", tagValue.TagName);
Assert.IsType<int>(tagValue.Value);
}
[Fact]
public async Task ReadTagAsync_WhenNotConnected_ShouldThrow()
{
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(
() => _driver.ReadTagAsync("Temperature")
);
}
[Fact]
public async Task WriteTagAsync_ShouldReturnTrue()
{
// Arrange
await _driver.ConnectAsync();
// Act
var result = await _driver.WriteTagAsync("SetPoint", 75.0);
// Assert
Assert.True(result);
}
}
Step 4: 빌드 및 테스트
# 의존성 복원
dotnet restore
# 빌드
dotnet build
# 테스트 실행
dotnet test
예상 출력:
테스트 실행 중...
✅ ConnectAsync_ShouldReturnTrue 통과
✅ ReadTagAsync_WhenConnected_ShouldReturnValue 통과
✅ ReadTagAsync_WhenNotConnected_ShouldThrow 통과
✅ WriteTagAsync_ShouldReturnTrue 통과
총 테스트: 4, 통과: 4, 실패: 0
Step 5: 실제 장비 통신 구현
Modbus TCP 예제
using NModbus;
using System.Net.Sockets;
private TcpClient? _tcpClient;
private IModbusMaster? _modbusMaster;
public async Task<bool> ConnectAsync(CancellationToken cancellationToken = default)
{
try
{
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync("192.168.1.100", 502, cancellationToken);
var factory = new ModbusFactory();
_modbusMaster = factory.CreateMaster(_tcpClient);
_isConnected = true;
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Modbus 연결 실패");
return false;
}
}
private async Task<object> ReadFromEquipmentAsync(
string tagName,
CancellationToken cancellationToken)
{
// Modbus 레지스터 읽기
ushort startAddress = 40001; // 예시
ushort[] registers = await Task.Run(
() => _modbusMaster!.ReadHoldingRegisters(1, startAddress, 1),
cancellationToken
);
return registers[0];
}
🎓 배운 내용
1. IDriver 인터페이스
public interface IDriver
{
Task<bool> ConnectAsync(CancellationToken cancellationToken = default);
Task DisconnectAsync();
Task<TagValue?> ReadTagAsync(string tagName, CancellationToken cancellationToken = default);
Task<bool> WriteTagAsync(string tagName, object value, CancellationToken cancellationToken = default);
}
2. 연결 관리
- 연결 상태 추적 (
_isConnected) - 에러 처리 및 로깅
- 리소스 정리 (
DisconnectAsync)
3. 태그 처리
- 태그 읽기 로직
- 태그 쓰기 로직
- 데이터 품질 관리 (
TagQuality)
🔍 모범 사례
1. 에러 처리
try
{
// 장비 통신
}
catch (TimeoutException ex)
{
_logger.LogWarning("통신 시간 초과");
return new TagValue(tagName, 0, TagQuality.Uncertain);
}
catch (Exception ex)
{
_logger.LogError(ex, "통신 오류");
return new TagValue(tagName, 0, TagQuality.Bad);
}
2. 재연결 로직
private async Task<bool> EnsureConnectedAsync()
{
if (_isConnected) return true;
_logger.LogWarning("연결 끊김 감지, 재연결 시도");
return await ConnectAsync();
}
3. 성능 최적화
// 배치 읽기 지원
public async Task<Dictionary<string, TagValue>> ReadTagsAsync(string[] tagNames)
{
// 한 번의 통신으로 여러 태그 읽기
var results = new Dictionary<string, TagValue>();
// 실제 구현...
return results;
}
🚀 다음 단계
다음 튜토리얼:
추가 학습:
- Caffeine.Core API - 전체 API 레퍼런스
- 드라이버 개발 가이드 - 고급 기법
완료 시간: 약 30분
난이도: ⭐⭐ 중급
이전: ← Hello Caffeine | 다음: 데이터 파이프라인 →