본문으로 건너뛰기

드라이버 생성 가이드

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