본문으로 건너뛰기

첫 번째 드라이버 만들기

Caffeine Framework로 커스텀 드라이버를 개발하는 방법을 배웁니다.

🎯 학습 목표

  • IDriver 인터페이스 구현
  • 태그 읽기/쓰기 로직 작성
  • 연결 관리 및 에러 처리
  • 드라이버 테스트

예상 소요 시간: 30분


📋 사전 요구사항


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

🚀 다음 단계

다음 튜토리얼:

추가 학습:


완료 시간: 약 30분
난이도: ⭐⭐ 중급
이전: ← Hello Caffeine | 다음: 데이터 파이프라인 →