드라이버 생성 가이드
Caffeine Framework로 산업용 프로토콜 드라이버를 개발하는 상세 가이드입니다.
📋 개요
이 가이드는 IDriver 인터페이스를 구현하여 커스텀 드라이버를 만드는 전체 과정을 다룹니다.
학습 시간: 약 1시간
난이도: ⭐⭐ 중급
🎯 드라이버 개발 프로세스
Step 1: 요구사항 분석
프로토콜 이해
드라이버 개발 전 프로토콜을 완전히 이해해야 합니다:
필수 확인 사항:
- ✅ 통신 방식 (TCP, UDP, Serial, HTTP 등)
- ✅ 데이터 포맷 (Binary, Text, JSON)
- ✅ 메시지 구조 (헤더, 바디, CRC)
- ✅ 에러 처리 방식
- ✅ 타임아웃 설정
예시 - Modbus TCP:
통신: TCP/IP, Port 502
포맷: Binary (Big-Endian)
메시지: [Transaction ID][Protocol ID][Length][Unit ID][Function Code][Data]
타임아웃: 1000ms
Step 2: 프로젝트 생성
CLI로 생성
# 테스트 포함 드라이버 프로젝트
cafe init --name MyProtocolDriver --template driver-full --git
cd MyProtocolDriver
dotnet restore
수동 생성
# 디렉토리 생성
mkdir MyProtocolDriver
cd MyProtocolDriver
# src 프로젝트
dotnet new classlib -o src/MyProtocolDriver
cd src/MyProtocolDriver
# Caffeine.Core 패키지 추가
dotnet add package NEXCODE.Caffeine.Core -v 2.0.*
#tests 프로젝트
cd ../..
dotnet new xunit -o tests/MyProtocolDriver.Tests
cd tests/MyProtocolDriver.Tests
# 프로젝트 참조 추가
dotnet add reference ../../src/MyProtocolDriver/MyProtocolDriver.csproj
Step 3: IDriver 인터페이스 구현
기본 구조
using Caffeine.Core.Abstractions;
using Caffeine.Core.Models;
using Microsoft.Extensions.Logging;
namespace MyProtocolDriver;
public class MyProtocolDriver : IDriver
{
private readonly ILogger<MyProtocolDriver> _logger;
private readonly DriverConfiguration _config;
private TcpClient? _tcpClient;
private NetworkStream? _stream;
private bool _isConnected;
public MyProtocolDriver(
DriverConfiguration config,
ILogger<MyProtocolDriver> logger)
{
_config = config;
_logger = logger;
}
public async Task<bool> ConnectAsync(CancellationToken cancellationToken = default)
{
// 구현
}
public async Task DisconnectAsync()
{
// 구현
}
public async Task<TagValue?> ReadTagAsync(
string tagName,
CancellationToken cancellationToken = default)
{
// 구현
}
public async Task<bool> WriteTagAsync(
string tagName,
object value,
CancellationToken cancellationToken = default)
{
// 구현
}
}
ConnectAsync 구현
public async Task<bool> ConnectAsync(CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("Connecting to {Host}:{Port}",
_config.Host, _config.Port);
// TCP 연결
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(_config.Host, _config.Port, cancellationToken);
_stream = _tcpClient.GetStream();
// 연결 확인 메시지 (프로토콜마다 다름)
var handshakeResult = await SendHandshakeAsync(cancellationToken);
if (!handshakeResult)
{
_logger.LogWarning("Handshake failed");
await DisconnectAsync();
return false;
}
_isConnected = true;
_logger.LogInformation("✅ Connected successfully");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Connection failed");
return false;
}
}
ReadTagAsync 구현
public async Task<TagValue?> ReadTagAsync(
string tagName,
CancellationToken cancellationToken = default)
{
if (!_isConnected)
{
throw new InvalidOperationException("Not connected");
}
try
{
// 1. 태그명 → 주소 변환
var address = ParseTagAddress(tagName);
// 2. 읽기 메시지 생성
var request = BuildReadRequest(address);
// 3. 전송
await _stream!.WriteAsync(request, cancellationToken);
// 4. 응답 수신
var response = await ReadResponseAsync(cancellationToken);
// 5. 응답 파싱
var value = ParseReadResponse(response);
// 6. TagValue 생성
return new TagValue(tagName, value, TagQuality.Good);
}
catch (TimeoutException)
{
_logger.LogWarning("Read timeout: {TagName}", tagName);
return new TagValue(tagName, 0, TagQuality.Uncertain);
}
catch (Exception ex)
{
_logger.LogError(ex, "Read failed: {TagName}", tagName);
return new TagValue(tagName, 0, TagQuality.Bad);
}
}
WriteTagAsync 구현
public async Task<bool> WriteTagAsync(
string tagName,
object value,
CancellationToken cancellationToken = default)
{
if (!_isConnected)
{
throw new InvalidOperationException("Not connected");
}
try
{
// 1. 태그명 → 주소 변환
var address = ParseTagAddress(tagName);
// 2. 값 검증 및 변환
var convertedValue = ConvertValue(value, address.DataType);
// 3. 쓰기 메시지 생성
var request = BuildWriteRequest(address, convertedValue);
// 4. 전송
await _stream!.WriteAsync(request, cancellationToken);
// 5. 응답 확인
var response = await ReadResponseAsync(cancellationToken);
// 6. 성공 여부 확인
var success = ValidateWriteResponse(response);
if (success)
{
_logger.LogInformation("✅ Write success: {TagName} = {Value}",
tagName, value);
}
return success;
}
catch (Exception ex)
{
_logger.LogError(ex, "Write failed: {TagName}", tagName);
return false;
}
}
DisconnectAsync 구현
public async Task DisconnectAsync()
{
if (!_isConnected)
{
return;
}
try
{
// 연결 해제 메시지 전송 (프로토콜마다 다름)
if (_stream != null)
{
await SendDisconnectMessageAsync();
}
_stream?.Close();
_tcpClient?.Close();
_isConnected = false;
_logger.LogInformation("Disconnected");
}
catch (Exception ex)
{
_logger.LogError(ex, "Disconnect error");
}
finally
{
_stream = null;
_tcpClient = null;
}
}
Step 4: 헬퍼 메서드 구현
주소 파싱
private Address ParseTagAddress(string tagName)
{
// 예: "DB1.DBW100" → Database 1, Word 100
var parts = tagName.Split('.');
return new Address
{
AreaCode = ParseAreaCode(parts[0]), // "DB1" → 0x84
Offset = ParseOffset(parts[1]), // "DBW100" → 100
DataType = ParseDataType(parts[1]) // "DBW" → Word
};
}
메시지 생성
private byte[] BuildReadRequest(Address address)
{
using var ms = new MemoryStream();
using var writer = new BinaryWriter(ms);
// 프로토콜별 메시지 구성
writer.Write((ushort)0x0001); // Transaction ID
writer.Write((ushort)0x0000); // Protocol ID (Modbus = 0)
writer.Write((ushort)6); // Length
writer.Write((byte)1); // Unit ID
writer.Write((byte)3); // Function Code (Read Holding Registers)
writer.Write((ushort)address.Offset);
writer.Write((ushort)1); // Quantity
return ms.ToArray();
}
응답 읽기
private async Task<byte[]> ReadResponseAsync(CancellationToken cancellationToken)
{
var buffer = new byte[1024];
var totalRead = 0;
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromMilliseconds(_config.Timeout));
try
{
// 헤더 읽기 (최소 6바이트)
while (totalRead < 6)
{
var read = await _stream!.ReadAsync(
buffer.AsMemory(totalRead, 6 - totalRead),
cts.Token);
if (read == 0)
{
throw new IOException("Connection closed");
}
totalRead += read;
}
// 길이 필드에서 남은 바이트 수 확인
var length = (buffer[4] << 8) | buffer[5];
// 나머지 읽기
while (totalRead < 6 + length)
{
var read = await _stream.ReadAsync(
buffer.AsMemory(totalRead, 6 + length - totalRead),
cts.Token);
if (read == 0)
{
throw new IOException("Connection closed");
}
totalRead += read;
}
return buffer[..totalRead];
}
catch (OperationCanceledException)
{
throw new TimeoutException("Read timeout");
}
}
Step 5: 테스트 작성
단위 테스트
using Xunit;
using Microsoft.Extensions.Logging.Abstractions;
namespace MyProtocolDriver.Tests;
public class MyProtocolDriverTests
{
private readonly MyProtocolDriver _driver;
public MyProtocolDriverTests()
{
var config = new DriverConfiguration
{
Host = "localhost",
Port = 502,
Timeout = 1000
};
_driver = new MyProtocolDriver(
config,
NullLogger<MyProtocolDriver>.Instance);
}
[Fact]
public async Task ConnectAsync_ShouldReturnTrue_WhenServerAvailable()
{
// Arrange
// (테스트 서버 시작)
// Act
var result = await _driver.ConnectAsync();
// Assert
Assert.True(result);
}
[Fact]
public async Task ReadTagAsync_ShouldReturnValue_WhenConnected()
{
// Arrange
await _driver.ConnectAsync();
// Act
var tagValue = await _driver.ReadTagAsync("DB1.DBW100");
// Assert
Assert.NotNull(tagValue);
Assert.Equal(TagQuality.Good, tagValue.Quality);
}
[Fact]
public async Task WriteTagAsync_ShouldReturnTrue_WhenConnected()
{
// Arrange
await _driver.ConnectAsync();
// Act
var result = await _driver.WriteTagAsync("DB1.DBW100", 1234);
// Assert
Assert.True(result);
}
}
Step 6: 성능 최적화
배치 읽기/쓰기
public async Task<Dictionary<string, TagValue>> ReadTagsAsync(
string[] tagNames,
CancellationToken cancellationToken = default)
{
// 연속된 주소는 하나의 요청으로 묶기
var groups = GroupConsecutiveAddresses(tagNames);
var results = new Dictionary<string, TagValue>();
foreach (var group in groups)
{
var values = await ReadMultipleAsync(group, cancellationToken);
foreach (var (tag, value) in values)
{
results[tag] = value;
}
}
return results;
}
연결 풀링
public class DriverPool
{
private readonly SemaphoreSlim _semaphore;
private readonly List<MyProtocolDriver> _drivers = new();
public DriverPool(int poolSize)
{
_semaphore = new SemaphoreSlim(poolSize, poolSize);
for (int i = 0; i < poolSize; i++)
{
_drivers.Add(CreateDriver());
}
}
public async Task<T> ExecuteAsync<T>(Func<MyProtocolDriver, Task<T>> action)
{
await _semaphore.WaitAsync();
try
{
var driver = _drivers[0]; // 간단한 예시
return await action(driver);
}
finally
{
_semaphore.Release();
}
}
}
📚 참고 자료
작성일: 2026-01-28
버전: 2.0